<?php
class helper_headScript extends system_view_helper_placeholder_container_standalone {
	/**#@+
	 * Script type contants
	 * @const string
	 */
	const FILE = 'FILE';
	const SCRIPT = 'SCRIPT';
	/**#@-*/
	
	/**
	 * Registry key for placeholder
	 * @var string
	 */
	protected $_key = 'headScript';
	
	/**
	 * Are arbitrary attributes allowed?
	 * @var bool
	 */
	protected $_arbitraryAttributes = false;
	
	/**#@+
	 * Capture type and/or attributes (used for hinting during capture)
	 * @var string
	 */
	protected $_captureLock;
	protected $_captureScriptType = null;
	protected $_captureScriptAttrs = null;
	protected $_captureType;
	/**#@-*/
	
	/**
	 * Optional allowed attributes for script tag
	 * @var array
	 */
	protected $_optionalAttributes = array('charset', 'defer', 'language', 'src');
	
	/**
	 * Required attributes for script tag
	 * @var string
	 */
	protected $_requiredAttributes = array('type');
	
	/**
	 * Whether or not to format scripts using CDATA; used only if doctype
	 * helper is not accessible
	 * @var bool
	 */
	public $useCdata = false;
	
	/**
	 * Constructor
	 *
	 * Set separator to PHP_EOL.
	 *
	 * @return void
	 */
	public function __construct() {
		parent::__construct();
		$this->setSeparator(PHP_EOL);
	}
	
	/**
	 * Return headScript object
	 *
	 * Returns headScript helper object; optionally, allows specifying a script
	 * or script file to include.
	 *
	 * @param  string $mode Script or file
	 * @param  string $spec Script/url
	 * @param  string $placement Append, prepend, or set
	 * @param  array $attrs Array of script attributes
	 * @param  string $type Script type and/or array of script attributes
	 * @return helper_headScript
	 */
	public function headScript($mode = helper_headScript::FILE, $spec = null, $placement = 'APPEND', array $attrs = array(), $type = 'text/javascript') {
		if((null!==$spec)&&is_string($spec)) {
			$action = ucfirst(strtolower($mode));
			$placement = strtolower($placement);
			switch($placement) {
				case 'set':
				case 'prepend':
				case 'append':
					$action = $placement.$action;
					break;
				default:
					$action = 'append'.$action;
					break;
			}
			$this->$action($spec, $type, $attrs);
		}
		
		return $this;
	}
	
	/**
	 * Start capture action
	 *
	 * @param  mixed $captureType
	 * @param  string $typeOrAttrs
	 * @return void
	 */
	public function captureStart($captureType = system_view_helper_placeholder_container_abstract::APPEND, $type = 'text/javascript', $attrs = array()) {
		if($this->_captureLock) {
			$e = new system_view_exception('Cannot nest headScript captures');
			$e->setView($this->view);
			throw $e;
		}
		
		$this->_captureLock = true;
		$this->_captureType = $captureType;
		$this->_captureScriptType = $type;
		$this->_captureScriptAttrs = $attrs;
		ob_start();
	}
	
	/**
	 * End capture action and store
	 *
	 * @return void
	 */
	public function captureEnd() {
		$content = ob_get_clean();
		$type = $this->_captureScriptType;
		$attrs = $this->_captureScriptAttrs;
		$this->_captureScriptType = null;
		$this->_captureScriptAttrs = null;
		$this->_captureLock = false;
		
		switch($this->_captureType) {
			case system_view_helper_placeholder_container_abstract::SET:
			case system_view_helper_placeholder_container_abstract::PREPEND:
			case system_view_helper_placeholder_container_abstract::APPEND:
				$action = strtolower($this->_captureType).'Script';
				break;
			default:
				$action = 'appendScript';
				break;
		}
		$this->$action($content, $type, $attrs);
	}
	
	/**
	 * Overload method access
	 *
	 * Allows the following method calls:
	 * - appendFile($src, $type = 'text/javascript', $attrs = array())
	 * - offsetSetFile($index, $src, $type = 'text/javascript', $attrs = array())
	 * - prependFile($src, $type = 'text/javascript', $attrs = array())
	 * - setFile($src, $type = 'text/javascript', $attrs = array())
	 * - appendScript($script, $type = 'text/javascript', $attrs = array())
	 * - offsetSetScript($index, $src, $type = 'text/javascript', $attrs = array())
	 * - prependScript($script, $type = 'text/javascript', $attrs = array())
	 * - setScript($script, $type = 'text/javascript', $attrs = array())
	 *
	 * @param  string $method
	 * @param  array $args
	 * @return Zend_View_Helper_HeadScript
	 * @throws Zend_View_Exception if too few arguments or invalid method
	 */
	public function __call($method, $args) {
		if(preg_match('/^(?P<action>set|(ap|pre)pend|offsetSet)(?P<mode>File|Script)$/', $method, $matches)) {
			if(1>count($args)) {
				$e = new system_view_exception(sprintf('Method "%s" requires at least one argument', $method));
				$e->setView($this->view);
				throw $e;
			}
			$action = $matches['action'];
			
			$mode = strtolower($matches['mode']);
			$type = 'text/javascript';
			$attrs = array();
			if('offsetSet'==$action) {
				$index = array_shift($args);
				if(1>count($args)) {
					$e = new system_view_exception(sprintf('Method "%s" requires at least two arguments, an index and source', $method));
					$e->setView($this->view);
					throw $e;
				}
			}
			
			$content = $args[0];
			
			if(isset($args[1])) {
				$type = (string) $args[1];
			}
			if(isset($args[2])) {
				$attrs = (array) $args[2];
			}
			
			switch($mode) {
				case 'script':
					$item = $this->createData($type, $attrs, $content);
					if('offsetSet'==$action) {
						$this->offsetSet($index, $item);
					} else {
						$this->$action($item);
					}
					break;
				case 'file':
				default:
					if(!$this->_isDuplicate($content)) {
						$content = preg_match('#^(https|http)://#', $content) ? $content : $this->view->baseUrl().$content;
						$attrs['src'] = $content;
						$item = $this->createData($type, $attrs);
						if('offsetSet'==$action) {
							$this->offsetSet($index, $item);
						} else {
							$this->$action($item);
						}
					}
					break;
			}
			
			return $this;
		}
		
		return parent::__call($method, $args);
	}
	
	/**
	 * Is the file specified a duplicate?
	 *
	 * @param  string $file
	 * @return bool
	 */
	protected function _isDuplicate($file) {
		foreach($this->getContainer() as $item) {
			if(($item->source===null)&&array_key_exists('src', $item->attributes)&&($file==$item->attributes['src'])) {
				return true;
			}
		}
		return false;
	}
	
	/**
	 * Is the script provided valid?
	 *
	 * @param  mixed $value
	 * @param  string $method
	 * @return bool
	 */
	protected function _isValid($value) {
		if((!$value instanceof stdClass)||!isset($value->type)||(!isset($value->source)&&!isset($value->attributes))) {
			return false;
		}
		
		return true;
	}
	
	/**
	 * Override append
	 *
	 * @param  string $value
	 * @return void
	 */
	public function append($value) {
		if(!$this->_isValid($value)) {
			$e = new system_view_exception('Invalid argument passed to append(); please use one of the helper methods, appendScript() or appendFile()');
			$e->setView($this->view);
			throw $e;
		}
		
		return $this->getContainer()->append($value);
	}
	
	/**
	 * Override prepend
	 *
	 * @param  string $value
	 * @return void
	 */
	public function prepend($value) {
		if(!$this->_isValid($value)) {
			$e = new system_view_exception('Invalid argument passed to prepend(); please use one of the helper methods, prependScript() or prependFile()');
			$e->setView($this->view);
			throw $e;
		}
		
		return $this->getContainer()->prepend($value);
	}
	
	/**
	 * Override set
	 *
	 * @param  string $value
	 * @return void
	 */
	public function set($value) {
		if(!$this->_isValid($value)) {
			$e = new system_view_exception('Invalid argument passed to set(); please use one of the helper methods, setScript() or setFile()');
			$e->setView($this->view);
			throw $e;
		}
		
		return $this->getContainer()->set($value);
	}
	
	/**
	 * Override offsetSet
	 *
	 * @param  string|int $index
	 * @param  mixed $value
	 * @return void
	 */
	public function offsetSet($index, $value) {
		if(!$this->_isValid($value)) {
			$e = new system_view_exception('Invalid argument passed to offsetSet(); please use one of the helper methods, offsetSetScript() or offsetSetFile()');
			$e->setView($this->view);
			throw $e;
		}
		
		return $this->getContainer()->offsetSet($index, $value);
	}
	
	/**
	 * Set flag indicating if arbitrary attributes are allowed
	 *
	 * @param  bool $flag
	 * @return helper_headScript
	 */
	public function setAllowArbitraryAttributes($flag) {
		$this->_arbitraryAttributes = (bool) $flag;
		return $this;
	}
	
	/**
	 * Are arbitrary attributes allowed?
	 *
	 * @return bool
	 */
	public function arbitraryAttributesAllowed() {
		return $this->_arbitraryAttributes;
	}
	
	/**
	 * Create script HTML
	 *
	 * @param  string $type
	 * @param  array $attributes
	 * @param  string $content
	 * @param  string|int $indent
	 * @return string
	 */
	public function itemToString($item, $indent, $escapeStart, $escapeEnd) {
		$attrString = '';
		if(!empty($item->attributes)) {
			foreach($item->attributes as $key => $value) {
				if(!$this->arbitraryAttributesAllowed()&&!in_array($key, $this->_optionalAttributes)) {
					continue;
				}
				if('defer'==$key) {
					$value = 'defer';
				}
				$attrString .= sprintf(' %s="%s"', $key, ($this->_autoEscape) ? $this->_escape($value) : $value);
			}
		}
		
		$type = ($this->_autoEscape) ? $this->_escape($item->type) : $item->type;
		$html = '<script type="'.$type.'"'.$attrString.'>';
		if(!empty($item->source)) {
			$html .= PHP_EOL.$indent.'    '.$escapeStart.PHP_EOL.$item->source.$indent.'    '.$escapeEnd.PHP_EOL.$indent;
		}
		$html .= '</script>';
		
		if(isset($item->attributes['conditional'])&&!empty($item->attributes['conditional'])&&is_string($item->attributes['conditional'])) {
			$html = $indent.'<!--[if '.$item->attributes['conditional'].']> '.$html.'<![endif]-->';
		} else {
			$html = $indent.$html;
		}
		
		return $html;
	}
	
	/**
	 * Retrieve string representation
	 *
	 * @param  string|int $indent
	 * @return string
	 */
	public function toString($indent = null) {
		$indent = (null!==$indent) ? $this->getWhitespace($indent) : $this->getIndent();
		
		$useCdata = $this->useCdata ? true : false;
		
		$escapeStart = ($useCdata) ? '//<![CDATA[' : '//<!--';
		$escapeEnd = ($useCdata) ? '//]]>' : '//-->';
		
		$items = array();
		$this->getContainer()->ksort();
		foreach($this as $item) {
			if(!$this->_isValid($item)) {
				continue;
			}
			
			$items[] = $this->itemToString($item, $indent, $escapeStart, $escapeEnd);
		}
		
		$return = implode($this->getSeparator(), $items);
		return $return;
	}
	
	/**
	 * Create data item containing all necessary components of script
	 *
	 * @param  string $type
	 * @param  array $attributes
	 * @param  string $content
	 * @return stdClass
	 */
	public function createData($type, array $attributes, $content = null) {
		$data = new stdClass();
		$data->type = $type;
		$data->attributes = $attributes;
		$data->source = $content;
		return $data;
	}
}